﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;

namespace QueuingSimulation
{
    public class Simulation
    {
        #region Private Members

        private int _clientID;
        private readonly Random _random = new Random();
        private Timer _simulationTimer;
        private int _maxQueueSize;
        private readonly SimulationType _simulationType;
        private double _timerInterval;
        private bool _clientStateChangeEventAdded;

        #endregion

        #region Properties

        public ClientEnterRangeTime ClientEnterRangeTime { get; set; }
        public int TotalWork { get; set; }
        public Queue<Client> Queue { get; set; }
        public List<Client> Clients { get; set; }
        public List<Server> Servers { get; set; }
        public int TimerValue;
        public double TimerInterval
        {
            get { return _timerInterval; }
            set
            {
                _timerInterval = value;
                _simulationTimer = new Timer(value);
            }
        }

        #endregion

        #region Events

        public event EventHandler<ClientStateChangeEventArgs> ClientStateChange;
        public event EventHandler SimulationEnded;

        protected virtual void OnSimulationEnded()
        {
            EventHandler handler = SimulationEnded;
            if (handler != null) handler(this, EventArgs.Empty);
        }

        protected virtual void OnClientStateChange(ClientStateChangeEventArgs e)
        {
            EventHandler<ClientStateChangeEventArgs> handler = ClientStateChange;
            if (handler != null) handler(this, e);
        }

        #endregion

        public Simulation(SimulationType type, double timerInterval = 100)
        {
            Servers = new List<Server>();
            TimerInterval = timerInterval;
            _simulationType = type;
            _simulationTimer.Elapsed += SimulationTimerOnElapsed;
        }

        #region Private Methods

        private void OnClientStateChange(object sender, ClientStateChangeEventArgs e)
        {
            switch (e.ClientState)
            {
                case ClientState.Arrive:
                    var allocated = ServerAllocation(e.Client);
                    if (!allocated)
                    {
                        Queue.Enqueue(e.Client);
                        if (Queue.Count > _maxQueueSize) _maxQueueSize = Queue.Count;
                        var args = new ClientStateChangeEventArgs
                        {
                            ClientState = ClientState.MoveToQueue,
                            Time = TimerValue,
                            Client = e.Client
                        };
                        OnClientStateChange(args);
                    }
                    Console.WriteLine("Client arrive id: " + e.Client.ID + " move to queue :" + !allocated + " at: " + TimerValue);
                    break;
            }
        }

        private void SimulationTimerOnElapsed(object sender, ElapsedEventArgs e)
        {
            foreach (var client in Clients.Where(c => c.EnterTime == TimerValue))
            {
                var args = new ClientStateChangeEventArgs
                {
                    ClientState = ClientState.Arrive,
                    Time = TimerValue,
                    Client = client
                };
                OnClientStateChange(args);
            }
            if (Clients.Count(c => c.ExitTime == 0) == 0)
            {
                _simulationTimer.Stop();
                OnSimulationEnded();
            }
            TimerValue++;
        }

        private void InitServers()
        {
            //Init servers
            Servers.ForEach(delegate(Server server)
            {
                server.StateChange += ServerOnStateChange;
                server.TimerInterval = TimerInterval;
                server.Reset();
                server.Timer.Start();
            });
        }

        private void ServerOnStateChange(object sender, ServerStateChangeEventArgs e)
        {
            var server = (Server)sender;

            Console.WriteLine(server.Name + " change state to : " + server.ServerState + " at: " + TimerValue);
            if (server.ServerState == ServerState.Free)
            {
                var args = new ClientStateChangeEventArgs
                {
                    ClientState = ClientState.ServiceEnd,
                    Time = TimerValue,
                    Client = e.Client
                };
                OnClientStateChange(args);
                if (Queue.Count != 0)
                {
                    var client = Queue.Dequeue();
                    Console.WriteLine(server.Name + " allocated to queue for client id : " + client.ID + " at: " + TimerValue);
                    ServerAllocation(client);
                }
            }
        }

        private void InitClients()
        {
            Clients = new List<Client>();
            if (_simulationType == SimulationType.ClientCountBase)
            {
                for (int i = 0; i < TotalWork; i++)
                {
                    AddClient();
                }
            }
            else
            {
                while (true)
                {

                    if (Clients.Count > 0 && Clients.Last().EnterTime > TotalWork) break;
                    AddClient();
                }
            }
        }

        private void AddClient()
        {
            var rndEnterTime = _random.Next(ClientEnterRangeTime.From, ClientEnterRangeTime.To);
            var lastClientEnter = Clients.Count == 0 ? 0 : Clients[_clientID - 1].EnterTime;
            var arriveTime = rndEnterTime + lastClientEnter;
            var client = new Client { ID = _clientID++, EnterTime = arriveTime };
            Clients.Add(client);
        }

        private bool ServerAllocation(Client client)
        {
            var freeServer = Servers.Where(c => c.ServerState == ServerState.Free).OrderBy(c => c.Priority).FirstOrDefault();

            if (freeServer != null)
            {
                Console.WriteLine(freeServer.Name + " allocated to client id:" + client.ID + " at: " + TimerValue);
                client.ServiceStartTime = TimerValue;
                client.DedicatedServer = freeServer.Name;
                freeServer.StarWork(client);
                var args = new ClientStateChangeEventArgs
                {
                    ClientState = ClientState.ServiceStart,
                    Time = TimerValue,
                    Client = client
                };
                OnClientStateChange(args);
                return true;
            }
            return false;
        }

        #endregion

        #region Public Methods

        public void Start()
        {
            if (!_clientStateChangeEventAdded)
            {
                ClientStateChange += OnClientStateChange;
                _clientStateChangeEventAdded = true;
            }
            InitServers();
            InitClients();
            Queue = new Queue<Client>();
            TimerValue = 0;
            _simulationTimer.Stop();
            _simulationTimer.Start();
        }

        public int GetTotalServerClients(Server server)
        {
            return Clients.Count(c => c.DedicatedServer == server.Name);
        }

        public int GetMaxQueueSize()
        {
            return _maxQueueSize;
        }

        #endregion
    }
}
